Skip to content

Investigate and reproduce NoMethodError: undefined method 'safe_concat' for nil on splash page with YJIT enabled#1

Open
ibrahima wants to merge 1 commit into
masterfrom
ibrahima/3-investigate-yjit-splash-page-error/6
Open

Investigate and reproduce NoMethodError: undefined method 'safe_concat' for nil on splash page with YJIT enabled#1
ibrahima wants to merge 1 commit into
masterfrom
ibrahima/3-investigate-yjit-splash-page-error/6

Conversation

@ibrahima
Copy link
Copy Markdown

Motivation / Background

Fixes a YJIT miscompilation bug where methods with more than 256 local variables caused integer truncation when mapping locals to registers. In Opnd::get_reg_opnd, a local index of 256 was cast to u8, wrapping to 0, causing YJIT to treat an untracked high-index local as tracked local 0. This led to register aliasing, memory corruption, and NoMethodError: undefined method 'safe_concat' for nil crashes on the Superconductor splash page when YJIT was enabled.

The root cause was traced to Slim's code generation: the old splash partial compiled into a single enormous method with enough locals (>256) to trigger the wrap. The bug reproduces on both Ruby 3.4.x and 4.0.x with YJIT enabled and is distinct from the previously fixed Ruby bug #21941.

Detail

This Pull Request changes:

  • yjit/src/backend/ir.rs: Replaces the truncating as u8 cast in Opnd::get_reg_opnd with a checked conversion (try_into().unwrap_or(u8::MAX)). Local indices that don't fit in u8 now resolve to u8::MAX (255), which RegMapping::alloc_reg correctly rejects as untrackable (since MAX_CTX_LOCALS is 8). This prevents oversized indices from wrapping into the trackable 0..7 range.

  • bootstraptest/test_yjit.rb: Adds a regression test that generates a Slim-shaped method with 128 repeated attribute blocks (~260+ locals), then calls it 40 times to trigger YJIT compilation. Before the fix, this fails around iteration 30 with NoMethodError; after the fix it completes cleanly.

Additional information

  • The fix only affects methods with >256 locals. For all normal-sized methods, behavior is unchanged.
  • There is no meaningful performance regression: the checked conversion runs at YJIT code-generation time (not per-instruction), and the affected locals were already outside YJIT's intended register-tracking window (>= MAX_CTX_LOCALS). The bug was accidentally granting unsound register allocation to wrapped indices, not a valid optimization being removed.
  • u8::MAX (255) is safe as a sentinel because RegMapping::alloc_reg gates on local_idx >= MAX_CTX_LOCALS as u8 (i.e., >= 8), so 255 is firmly rejected.

Checklist

  • This Pull Request is related to one change. Unrelated changes should be opened in separate PRs.
  • Commit message has a detailed description of what changed and why.
  • Tests are added or updated if you fix a bug or add a feature.
  • CHANGELOG files are updated for the changed libraries if there is a behavior change or additional feature.

Superconductor Ticket Implementation | App Preview | Guided Review

Copilot AI review requested due to automatic review settings May 19, 2026 01:02
@superconductor-for-github
Copy link
Copy Markdown

🔗 This pull request is linked to Superconductor implementation.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a YJIT register-mapping bug for methods with very large local tables by preventing oversized local indices from wrapping into tracked local register slots.

Changes:

  • Replaces a truncating as u8 cast in Opnd::get_reg_opnd with a checked conversion that maps out-of-range locals to an untracked sentinel.
  • Adds a bootstrap regression test intended to reproduce the large-local Slim-style method failure.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
yjit/src/backend/ir.rs Updates local operand register mapping to avoid u8 wraparound for high local indices.
bootstraptest/test_yjit.rb Adds a regression scenario with 256+ generated locals and repeated rendering calls.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread bootstraptest/test_yjit.rb Outdated
}

# regression test for register mapping of methods with over 256 locals
assert_normal_exit %q{
@superconductor-for-github superconductor-for-github Bot force-pushed the ibrahima/3-investigate-yjit-splash-page-error/6 branch 6 times, most recently from 42fb311 to e152e79 Compare May 19, 2026 23:53
Previously, local indices greater than 255 were truncated when
converted to RegOpnd::Local. This could make a high-index local alias
a tracked low-index local in the register mapping and produce incorrect
values after compilation.

Cap overflowing indices to u8::MAX. Since this is greater than
MAX_CTX_LOCALS, YJIT treats these locals as untracked.

Fixes [Bug #22074].

Co-authored-by: Codex <199175422+chatgpt-connector[bot]@users.noreply.github.com>
@superconductor-for-github superconductor-for-github Bot force-pushed the ibrahima/3-investigate-yjit-splash-page-error/6 branch from e152e79 to 0f35c62 Compare May 19, 2026 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants